#!/usr/bin/perl
# TODO: different bounce emails, different IMAP servers, different POP3 servers, connections closing, don't want to delete false positives

use strict;
use warnings;

use Readonly;
use Foundation;
use Mail::IMAPClient;
use Mail::POP3Client;
use Email::MIME::RFC2047::Decoder;
use Email::Address;
use Email::MIME;
use Date::Parse;
use Data::Dumper;
use E3DS::BounceParser;

use HelperAppUnixServerSocket;
use MailScanner::IMAP;
use MailScanner::POP3;
use MailScanner::Test;

Readonly my $EX_USAGE => 64;
Readonly my $EX_OK    => 0;

Readonly my @SCANNER_PACKAGES => ( "MailScanner::POP3", "MailScanner::IMAP", "MailScanner::Test" );

Readonly my $STATUS_CONNECTING    			 => "CONNECTING";
Readonly my $STATUS_CONNECTED     			 => "CONNECTED";
Readonly my $STATUS_LIST_MESSAGES 			 => "LIST_MESSAGES";
Readonly my $STATUS_MESSAGE_COUNT 			 => "MESSAGE_COUNT";
Readonly my $STATUS_DOWNLOAD_MESSAGE_HEADERS => "DOWNLOAD_MESSAGE_HEADERS";
Readonly my $STATUS_DOWNLOAD_MESSAGE_BODY    => "DOWNLOAD_MESSAGE_BODY";
Readonly my $STATUS_MESSAGE_DOWNLOADED       => "MESSAGE_DOWNLOADED";
Readonly my $STATUS_SUBSCRIBE	  		 	 => "SUBSCRIBE";
Readonly my $STATUS_UNSUBSCRIBE   			 => "UNSUBSCRIBE";
Readonly my $STATUS_BOUNCE		  			 => "BOUNCE";
Readonly my $STATUS_FINISHED 	  			 => "FINISHED";

Readonly my @BOUNCE_SUBJECT_REGEXES 		 => ( qr/Mail delivery failed/i,
	 											  qr/DELIVERY FAILURE/i,
	 											  qr/Delivery Status Notification/i,
												  qr/Delivery Notification/i,
	 											  qr/Mail Delivery Problem/i,
	 											  qr/Message status/i,
	 											  qr/Returned e?mail/i,
	 											  qr/Undeliverable Mail/i,
	 											  qr/Undeliverable:/i,
	 											  qr/Undelivered Mail Returned to Sender/i,
	 											  qr/failure notice/i,
	 											  qr/Delivery Error Report/i,
	 											  qr/Automatically rejected mail/i,
	 											  qr/Undeliverable Message/i,
												  qr/Non delivery report/i,
	 											  qr/non remis/i,
	 											  qr/Impossibile recapitare/i,
												  qr/Unzustellbar/i,
												  qr/\x{914D}\x{4FE1}\x{4E0D}\x{80FD}/i );

sub reportError
{
	my ( $sock, $error ) = @_;
		
	utf8::encode( $error );
		
	$sock->write( "ERROR: $error" );
}

sub reportStatus
{
	my $sock = shift @_;
	my @args = map { defined( $_ ) ? $_ : '' } @_;
				
	$sock->write( "STATUS: " . join( ',', map { $_ =~ s/"/\\"/g; utf8::encode( $_ ); $_; } @args ) );
}

sub subjectIsBounceNotification
{
	my $subject = shift @_;
	
	for my $regex ( @BOUNCE_SUBJECT_REGEXES ) {
		return 1 if ( $subject =~ m/$regex/ );
	}
	
	return 0;
}

sub main
{
	$| = 1;
	
	die if ( scalar @ARGV < 1 );

	my $sock = HelperAppUnixServerSocket->new( $ARGV[0] );
				
	my ( $config_data_len, $config_data ) = $sock->read();
		
	if ( !$config_data_len ) {
		die "Unable to read configuration data from host application.";
	}

	my $plist_string      = NSString->alloc()->initWithCString_encoding_( $config_data, 4 );
	my $plist_string_data = $plist_string->dataUsingEncoding_( 4 );
	my $plist 			  = NSPropertyListSerialization->propertyListWithData_options_format_error_( $plist_string_data, 0, 0, 0 );
	my $config     		  = undef;
	
	if ( $plist && $$plist ) {
		$config = Foundation::perlRefFromObjectRef( $plist );
	}
	
	if ( $config->{ VerboseLogging } ) {
		my $save_password = $config->{ Password };
		
		$config->{ Password } = "(hidden)";
	
		print "Read config: ", Dumper( $config ), "\n";
		
		$config->{ Password } = $save_password;
	}
		
	my $scanner_package = $SCANNER_PACKAGES[$config->{ Protocol }];
	my $scanner			= $scanner_package->new();
	my $error			= undef;
	
	reportStatus( $sock, $STATUS_CONNECTING );
		
	if ( !$scanner->connect( $config->{ Hostname }, $config->{ Port }, $config->{ UseSSL }, $config->{ User }, $config->{ Password }, \$error ) ) {
		reportError( $sock, "Couldn't connect to $config->{ Hostname }" );
		die $error;
	}
	
	reportStatus( $sock, $STATUS_CONNECTED );
	reportStatus( $sock, $STATUS_LIST_MESSAGES );
	
	my %message_number_to_uid;
	my @candidate_message_nums = $scanner->getCandidateMessageNumbers( $config, \%message_number_to_uid, \$error );
	
	if ( !@candidate_message_nums && $error ) {
		reportError( $sock, $error );
		die $error;
	}
	
	print "Candidate message numbers: ", Dumper( \@candidate_message_nums ), "\n" if $config->{ VerboseLogging };
	print "Message number to UID: ", Dumper( \%message_number_to_uid ), "\n" if $config->{ VerboseLogging };
	
	reportStatus( $sock, $STATUS_MESSAGE_COUNT, scalar @candidate_message_nums );
	
	my $total_handled = 0;
	my $limit         = $config->{ Limit };
	
	for my $msg_num ( @candidate_message_nums ) {
		my @req_headers	 = qw( Message-id Subject From Date );
		my $headers_ref	 = undef;
		my $message		 = undef;
		
		reportStatus( $sock, $STATUS_DOWNLOAD_MESSAGE_HEADERS, $msg_num );
		
		$headers_ref = $scanner->getMessageHeaders( $msg_num, \@req_headers, \$error );
					
		print "Couldn't get message headers: $error" if !$headers_ref && $config->{ VerboseLogging };
					
		next if !$headers_ref;

		print "Headers: ", Dumper( $headers_ref ), "\n" if $config->{ VerboseLogging };

		my $message_id = $headers_ref->{ "Message-id" }->[0];
		my $date       = str2time( $headers_ref->{ "Date" }->[0] );
				
		reportStatus( $sock, $STATUS_MESSAGE_DOWNLOADED, $message_id, $message_number_to_uid{ $msg_num }, $date );

		my ( $subject, $from );
		my $decoder   = Email::MIME::RFC2047::Decoder->new();
		my @addresses = Email::Address->parse( $headers_ref->{ From }->[0] );

		print "Addresses: ", Dumper( \@addresses ), "\n" if $config->{ VerboseLogging };

		if ( @addresses ) {
			$from = $addresses[0];
			$from->phrase( $decoder->decode_phrase( $from->phrase ) );
			
			print "From: ", $from->phrase, " ", $from->address, "\n" if $config->{ VerboseLogging };
		}

		if ( defined( $headers_ref->{ Subject } ) ) {
			$subject = $decoder->decode_text( $headers_ref->{ Subject }->[0] );	

			print "Subject: $subject", "\n" if $config->{ VerboseLogging };		
		}			
		
		my $handled = 0;
		
		if ( $config->{ LookForSubscribe } && $subject && $from ) {
			my $magic_phrase = $config->{ SubscribePhrase };
			
			if ( $subject =~ m/\b$magic_phrase\b/i ) {
				print "Subscribe magic phrase found\n" if $config->{ VerboseLogging };
				my $name = $from->phrase;
								
				reportStatus( $sock, $STATUS_SUBSCRIBE, $message_id, $date, $from->address, $name );
				
				if ( $config->{ DeleteSubscribed } ) {
					print "Deleting message $msg_num\n" if $config->{ VerboseLogging };
					$scanner->deleteMessage( $msg_num );
				}
				
				$handled = 1;
			}
			else {
				print "Subscribe magic phrase not found\n" if $config->{ VerboseLogging };
			}
		}
		
		if ( !$handled && $config->{ LookForUnsubscribe } && $subject && $from ) {
			my $magic_phrase = $config->{ UnsubscribePhrase };
			
			if ( $subject =~ m/\b$magic_phrase\b/i ) {
				print "Unsubscribe magic phrase found\n" if $config->{ VerboseLogging };
				
				my $address_id = undef;
				
				if ( $subject =~ m/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})/ ) {
					print "Found address ID $1\n" if $config->{ VerboseLogging };
					$address_id = $1;
				}
				
				reportStatus( $sock, $STATUS_UNSUBSCRIBE, $message_id, $date, $from->address, $address_id );
				
				if ( $config->{ DeleteUnsubscribed } ) {
					print "Deleting message $msg_num\n" if $config->{ VerboseLogging };
					$scanner->deleteMessage( $msg_num );
				}

				$handled = 1;
			}
			else {
				print "Unsubscribe magic phrase not found\n" if $config->{ VerboseLogging };
			}
		}
		
		if ( !$handled && $config->{ LookForBounced } && $subject && subjectIsBounceNotification( $subject ) ) {
			reportStatus( $sock, $STATUS_DOWNLOAD_MESSAGE_BODY, $message_id );

			my $message = $scanner->getRawMessage( $msg_num, \$error );
			
			if ( !$message ) {
				print "Couldn't get message: $error\n" if $config->{ VerboseLogging };
				next;
			}
			else {
				print "Looking for bounce in message $message\n" if $config->{ VerboseLogging };
			}
			
            my $bounce_report_ref = E3DS::BounceParser->parse( $message, $config->{ VerboseLogging } );
			
			if ( !$bounce_report_ref ) {
				next;
			}
			
			if ( !$bounce_report_ref->{ Email } ) {
				print "No bounced email address\n" if $config->{ VerboseLogging };
				next;
			}
			
			my $std_reason    = $bounce_report_ref->{ StandardReason } || 0;
			my $hard_bounce   = ( $std_reason eq 'user_unknown' || $std_reason eq 'domain_error' ) ? 1 : 0;
			my %ids_by_reason = (
				'user_unknown' 		=> 1,
				'user_disabled'     => 1,
				'over_quota' 		=> 2,
				'domain_error' 	    => 3,
				'domain_unknown'    => 3,
				'spam' 				=> 4,
				'message_too_large' => 5,
				'mail_loop'         => 3,
				'unknown' 			=> 0
			);
						
			reportStatus( 
				$sock, 
				$STATUS_BOUNCE, 
				$message_id, 
				$bounce_report_ref->{ Email },
				$bounce_report_ref->{ DeliveryID } || 0, 
				$hard_bounce, 
				$ids_by_reason{ $std_reason } || 0
			);
			$scanner->deleteMessage( $msg_num ) if $config->{ DeleteBounced };
			
			$handled = 1;
		}
		
		$total_handled += 1 if $handled;
		
		last if ( defined( $limit ) && $total_handled >= $limit );
	}
	
	$scanner->disconnect();
	
	reportStatus( $sock, $STATUS_FINISHED );
	
	$sock->close();
	
	return $EX_OK;
}

exit( main() );